import xml.etree.ElementTree as ET
import os
import shutil
import subprocess
import hashlib
import json
import re


# 全局变量
class Global:
    PWD = ''
    FULL_RELEASE = True
    APP_NAME = 'TlvTool'
    UPDATE_NAME = 'TlvToolUpdate'
    WORK_NAME = 'UpdateWorker'
    APP_VERSION = '1.0.0'
    SRC_EXE_PATH = ''
    QT_BUILD_DIR = ''
    DEST_DIR = ''
    DEST_OLD_DIR = ''
    

# 获取当前脚本所在目录
def get_current_dir():
    return os.path.dirname(os.path.abspath(__file__))


# 读取 src/global/const.h 中的第5行和第8行，判断是否为完整版发布，并获取版本号
def get_is_full_and_version():
    ret = [True, '1.0.0']
    with open(os.path.join('src', 'global', 'const.h'), 'r', encoding='utf-8') as f:
        lines = f.readlines()
        if len(lines) >= 5:
            line = lines[4].strip()
            if line.endswith('0'):
                print('根据 const.h 文件，设置为精简版发布')
                ret[0] = False
            else:
                print('根据 const.h 文件，设置为完整版发布')
                ret[0] = True
        # 读取版本号
        if len(lines) >= 8:
            line = lines[7].strip()
            m = re.search(r"#define\s+APP_VERSION\s+(0x[0-9A-Fa-f]+|\d+)", line)
            if not m:
                raise ValueError("APP_VERSION not found")
            ret[1] = int(m.group(1), 0)  # 0 表示自动识别进制
            print(f'根据 const.h 文件，获取版本号: {ret[1]}')
    f.close()
    return ret
        

# 获取 Qt 构建目录
def get_build_release_dir():
    xmlFile = ''
    for path in os.listdir('.'):
        if  path.endswith('.pro.user') and os.path.isfile(path):
            xmlFile = path
            
    if len(xmlFile) == 0:
        print('没有找到 .pro.user 文件')
        exit(1)


    # 读取 XML 文件
    tree = ET.parse(xmlFile)
    root = tree.getroot()

    # 查找 Release 目录
    buildDir = ''
    for value in root.iter('value'):
        if value.get('key') == 'ProjectExplorer.BuildConfiguration.BuildDirectory':
            if value.text.lower().endswith('release'):
                buildDir = value.text
                break
            
    if len(buildDir) == 0:
        print('没有找到 Release 目录')
        exit(1)
    else:
        print(f'找到 Release 目录: {buildDir}')
        return buildDir


def copy_exe(exe_path, dst):
    # 创建目标目录
    if not os.path.exists(dst):
        os.makedirs(dst)
        
    # 拷贝主程序
    shutil.copy(exe_path, dst)
    
    

# 拷贝public下的所有子目录和文件
def copy_public_dir(dst, is_full):
    src_public = os.path.join('public')
    shutil.copytree(src_public, dst, dirs_exist_ok=True)
    # 轻量版不拷贝nodejs
    if not is_full:
        dst_nodejs = os.path.join(dst, 'tools', 'nodejs')
        if os.path.exists(dst_nodejs):
            shutil.rmtree(dst_nodejs)
            


# 拷贝 qscintilla.dll等依赖项
def copy_others(dst):
    shutil.copy(os.path.join('lib', 'Qsci', 'qscintilla2_qt5.dll'), dst)
    shutil.copy(os.path.join('lib', 'OpenSSL', 'libcrypto-1_1-x64.dll'), dst)
    shutil.copy(os.path.join('lib', 'OpenSSL', 'libssl-1_1-x64.dll'), dst)
    
    

'''
对比新旧版本差异，若不存在旧版本，则不进行对比直接生成版本为0的清单文件，否则进行对比，并删除旧版本目录，重命名新版本目录，并在新版本目录下生成 MANIFEST 清单文件

清单文件格式：
{
    "app_version": "1.0.0",
    "files": [
        { "path": "path/to/file1", "ver": 0 },
        { "path": "path/to/file2", "ver": 3 },
        ...
    ]
}

对比规则：若新旧版本文件内容相同，则版本号不变，若不同则版本号加1，若新版本中存在而旧版本中不存在的文件，则版本号为0，若旧版本中存在而新版本中不存在的文件，则忽略该文件



思路整理：

输入参数：
    old_dir: 旧版本目录路径
    new_dir: 新版本目录路径

流程：
    如果 old_dir 不存在：
        遍历 new_dir，所有文件的版本号设为 0，生成 MANIFEST
    如果 old_dir 存在：
        读取旧版本的 MANIFEST（若有）
        对比 old_dir 和 new_dir：
            文件存在且内容相同 → 版本号不变
            文件存在但内容不同 → 版本号 +1
            新文件 → 版本号 0
            旧有但新无 → 忽略
        删除 old_dir
        将 new_dir 重命名为 old_dir（即覆盖旧版本目录）
        在重命名后的目录下生成 MANIFEST

辅助：
    用 hashlib.md5 来比较文件内容
    用 json 读写 MANIFEST

'''

def file_info(path, chunk_size=8192):
    """返回文件的 md5 和 size"""
    md5 = hashlib.md5()
    size = 0
    with open(path, 'rb') as f:
        while chunk := f.read(chunk_size):
            size += len(chunk)
            md5.update(chunk)
    return md5.hexdigest(), size

def generate_manifest(old_dir, new_dir, app_version):
    """
    对比新旧版本目录，生成升级版 MANIFEST
    """
    # 读取旧 manifest
    old_manifest_path = os.path.join(old_dir, 'MANIFEST')
    old_manifest = {}
    if os.path.exists(old_manifest_path):
        with open(old_manifest_path, 'r', encoding='utf-8') as f:
            old_data = json.load(f)
            for item in old_data.get("files", []):
                old_manifest[item['path']] = item

    new_files = []

    for root, dirs, files in os.walk(new_dir):
        for file in files:
            if file == 'MANIFEST':
                continue

            abs_path = os.path.join(root, file)
            rel_path = os.path.relpath(abs_path, new_dir).replace("\\", "/")

            new_hash, new_size = file_info(abs_path)

            if rel_path in old_manifest:
                old_item = old_manifest[rel_path]
                old_hash = old_item["hash"]

                if new_hash == old_hash:
                    ver = old_item["ver"]
                else:
                    ver = old_item["ver"] + 1
            else:
                ver = 0  # 新文件

            new_files.append({
                "path": rel_path,
                "ver": ver,
                "hash": new_hash,
                "size": new_size
            })

    # 删除旧目录
    if os.path.exists(old_dir):
        shutil.rmtree(old_dir)

    # 重命名新目录为旧目录
    shutil.move(new_dir, old_dir)

    # 写入新版 manifest
    manifest_data = {
        "app_version": app_version,
        "files": new_files
    }
    new_manifest_path = os.path.join(old_dir, 'MANIFEST')
    with open(new_manifest_path, 'w', encoding='utf-8') as f:
        json.dump(manifest_data, f, indent=4, ensure_ascii=False)

    print(f"已生成清单文件: {new_manifest_path}")
    return manifest_data



# 主函数    
def main():
    Global.PWD = get_current_dir()
    os.chdir(Global.PWD)
    
    Global.FULL_RELEASE, Global.APP_VERSION = get_is_full_and_version()
    
    Global.QT_BUILD_DIR = get_build_release_dir()
    
    # 配置路径
    Global.SRC_EXE_PATH = os.path.join(Global.QT_BUILD_DIR, 'release', f'{Global.APP_NAME}.exe')
    if Global.FULL_RELEASE:
        Global.DEST_DIR = os.path.join('dist', Global.APP_NAME + '_new')
        Global.DEST_OLD_DIR = os.path.join('dist', Global.APP_NAME)
    else:
        Global.DEST_DIR = os.path.join('dist', Global.APP_NAME + '_lite')
        
    copy_exe(Global.SRC_EXE_PATH, Global.DEST_DIR)
    
    copy_public_dir(Global.DEST_DIR, Global.FULL_RELEASE)
    copy_others(Global.DEST_DIR)

    print(f'Release build copied to {Global.DEST_DIR}')

    # 进入 DEST_DIR
    os.chdir(Global.DEST_DIR)

    # 运行 windeployqt
    subprocess.run(['windeployqt', f'{Global.APP_NAME}.exe', '--no-compiler-runtime', '--no-angle', '--no-opengl-sw'])

    # 返回初始路径
    os.chdir(Global.PWD)
    
    copy_exe(f'./dist/{Global.WORK_NAME}.exe', Global.DEST_DIR)
    copy_exe(f'./dist/{Global.UPDATE_NAME}.exe', Global.DEST_DIR)
    # os.remove(f'./dist/{Global.WORK_NAME}.exe')
    # os.remove(f'./dist/{Global.UPDATE_NAME}.exe')

    # 轻量版不生成清单文件
    if not Global.FULL_RELEASE:
        return
    
    if os.path.exists('./public/MANIFEST'):
        copy_exe('./public/MANIFEST', Global.DEST_OLD_DIR)
    
    generate_manifest(Global.DEST_OLD_DIR, Global.DEST_DIR, Global.APP_VERSION)
    
    os.chdir(Global.PWD)
    copy_exe(os.path.join(Global.DEST_OLD_DIR, 'MANIFEST'), './public')


if __name__ == '__main__':
    main()
